/**
 * \file
 */
#include "precompiled.hpp"
#include "kfs.hpp"
#include "kfs_cache.hpp"
#include "kfs_file_descriptor.hpp"
#include "kfs_impl.hpp"
#include "Exception.hpp"
#include <boost/bind.hpp>
#include <boost/static_assert.hpp>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>

using std::auto_ptr;  using std::string;
using std::vector;

enum
{
    DirEntrySize = 128,
    NameMaxSize = DirEntrySize - sizeof(block_index_t) - 1
};

struct DirEntry
{
    /** A pointer to the block on disk where the inode for this directory
    * entry is stored.
     */
    block_index_t inodeBlockNum;
    /**
     * The name of the entry.
     */
    char name[NameMaxSize + 1];
};

BOOST_STATIC_ASSERT(sizeof(DirEntry) == DirEntrySize);


void
kfs::Impl::dir_add(FileDescriptor* dir, string const& name,
    block_index_t childInodeNum)
{
    if(name.size() > NameMaxSize)
    {
        throw Error(ENAMETOOLONG, "File name too long");
    }
    bool alreadyExists;
    try
    {
        DirIter dirIter;
        dir_find(dir, name, &dirIter);
        alreadyExists = true;
    }
    catch(Error& e)
    {
        if( e.error() != ENOENT )
            throw e;
        alreadyExists = false;
    }
    CacheInode& inodeNode = *dir->inodeRef->elt->second;
    if(alreadyExists)
    {
        throw Error(EEXIST, "Error: directory %u: another entry exists with "
            "name \"%s\"", inodeNode.blockNum, name.c_str());
    }

    DirEntry entry;
    entry.inodeBlockNum = childInodeNum;
    strncpy(entry.name, name.c_str(), NameMaxSize+1);
    // strncpy does not null-terminate if name.size == NameMaxSize+1.
    entry.name[NameMaxSize] = '\0';

    // Append the entry at the end of the list.
    file_size_t dirOffset = inodeNode.inode.sizeNumBytes;
    size_t numBytes = dir->write(&entry, DirEntrySize, dirOffset);
    if(numBytes != DirEntrySize)
    {
        throw Error(ENOSPC, "?? Could not write new directory entry");
    }
}


void
kfs::Impl::dir_find(FileDescriptor const* dir, string const& desired,
    DirIter* iter)
{
    CacheInode const& inodeNode = *dir->inodeRef->elt->second;
    if( S_ISDIR(inodeNode.inode.mode) == 0 )
    {
        throw Error(ENOTDIR, "Error: inode %u is not a directory",
            inodeNode.blockNum);
    }

    file_size_t const end = inodeNode.inode.sizeNumBytes;
    for(file_size_t offset = 0; offset < end; offset += DirEntrySize)
    {
        DirEntry entry;
        if(dir->read(&entry, sizeof(entry), offset) != sizeof(entry))
        {
            // Premature end of file.
            break;
        }
        if(entry.inodeBlockNum != 0 && desired.compare(entry.name) == 0)
        {
            iter->offset = offset;
            iter->blockNum = entry.inodeBlockNum;
            return;
        }
    }
    throw Error(ENOENT,"directory %u: entry \"%s\" does not exist",
        inodeNode.blockNum, desired.c_str());
}


uint32_t
kfs::Impl::dir_numEntries(FileDescriptor const* dir)
{
    CacheInode const& inodeNode = *dir->inodeRef->elt->second;
    if( S_ISDIR(inodeNode.inode.mode) == 0 )
    {
        throw Error(ENOTDIR, "Error: inode %u is not a directory",
            inodeNode.blockNum);
    }
    if(inodeNode.inode.sizeNumBytes % DirEntrySize)
    {
        throw Exception("Error: directory %u has a partial entry",
            inodeNode.blockNum);
    }
    return inodeNode.inode.sizeNumBytes / DirEntrySize;
}


void
kfs::Impl::dir_remove(FileDescriptor* dir, file_size_t offset)
{
    Inode& dirInode = dir->inodeRef->elt->second->inode;
    if(offset + DirEntrySize > dirInode.sizeNumBytes)
    {
        // Offset is beyond the end of the list.
        throw Error(EINVAL, "%s: Invalid offset %lu", __FUNCTION__, offset);
    }
    else if(offset + DirEntrySize == dirInode.sizeNumBytes)
    {
        // Remove the final entry
        dir->truncate(offset);
    }
    else
    {
        // Remove an entry, not the final one.
        file_size_t finalOffset = dirInode.sizeNumBytes - DirEntrySize;
        assert(offset < finalOffset);
        DirEntry otherEntry;
        dir->read(&otherEntry, DirEntrySize, finalOffset);
        dir->write(&otherEntry, DirEntrySize, offset);
        dir->truncate(finalOffset);
    }
}


void
kfs::Impl::dir_visitEntries(FileDescriptor const* dir, file_size_t byteOffset,
    boost::function<bool(file_size_t, string const&)> visitEntry)
{
    CacheInode const& inodeNode = *dir->inodeRef->elt->second;
    if( S_ISDIR(inodeNode.inode.mode) == 0 )
    {
        throw Error(ENOTDIR,"Error: inode %u is not a directory",
            inodeNode.blockNum);
    }
    if(inodeNode.inode.sizeNumBytes % DirEntrySize)
    {
        throw Exception("Error: directory %u has a partial entry",
            inodeNode.blockNum);
    }
    if(byteOffset % DirEntrySize)
    {
        throw Error(EINVAL,"Error: directory %u: invalid offset argument",
            inodeNode.blockNum);
    }

    bool wantsMore = true;
    do
    {
        DirEntry dirEntry;
        size_t numBytes = dir->read(&dirEntry, DirEntrySize, byteOffset);
        if(numBytes != DirEntrySize)
        {
            break;
        }
        byteOffset += numBytes;
        // Visit the entry.  Give the offset of the next directory entry.
        wantsMore = visitEntry(byteOffset, dirEntry.name);
    }
    while(wantsMore);
}


bool
static
dir_safeToDelete_visitEntry(bool* isSafeToDelete, file_size_t offset,
    string const& entryName);

bool
kfs::Impl::dir_safeToDelete(FileDescriptor const* dir)
{
    CacheInode const& inodeNode = *dir->inodeRef->elt->second;
    if( S_ISDIR(inodeNode.inode.mode) == 0 )
    {
        throw Error(ENOTDIR, "Error: inode %u is not a directory",
            inodeNode.blockNum);
    }

    bool isSafeToDelete = true;
    dir_visitEntries(dir, 0,
        boost::bind( &dir_safeToDelete_visitEntry, &isSafeToDelete, _1, _2)
    );
    return isSafeToDelete;
}


bool
static
dir_safeToDelete_visitEntry(bool* isSafeToDelete, file_size_t offset,
    string const& entryName)
{
    static string const parent = "..";
    static string const self = ".";
    if(entryName == parent || entryName == self)
    {
        // Keep looking
        return true;
    }
    else
    {
        // Stop now.
        *isSafeToDelete = false;
        return false;
    }
}
